package com.tianle.flutter_app_install

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.core.content.FileProvider
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel.Result
import java.io.BufferedReader
import java.io.DataOutputStream
import java.io.File
import java.io.IOException
import java.util.*

class InstallUtils(private var applicationContext: Context, private var mActivity: Activity?) {

    /**
     * Detect if the device has root permission
     *
     * @return Boolean
     **/
    fun isDeviceRooted(call: MethodCall, result: Result) {
        val su = "su"
        val locations = arrayOf(
            "/system/bin/", "/system/xbin/", "/sbin/", "/system/sd/xbin/",
            "/system/bin/failsafe/", "/data/local/xbin/", "/data/local/bin/", "/data/local/"
        )
        var resultReplied = false
        for (location in locations) {
            if (File(location + su).exists()) {
                result.success(true)
                resultReplied = true
                break
            }
        }

        if (!resultReplied) result.success(false)
    }

    /**
     * Detect if the application has the same signature as the android system.
     *
     * @return Boolean
     **/
    fun isSystemApplication(packageManager: PackageManager?, packageName: String): Boolean {
        if (packageManager == null || packageName.isEmpty()) {
            return false
        }

        val getPackageInfoFlag: Int = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
            PackageManager.GET_SIGNATURES
        } else {
            PackageManager.GET_SIGNING_CERTIFICATES
        }

        // Get PackageInfo for target application
        val targetPkgInfo: PackageInfo = packageManager.getPackageInfo(
            packageName, getPackageInfoFlag
        )

        // Get PackageInfo for system package
        val sysPackageInfo: PackageInfo = packageManager.getPackageInfo(
            "android", getPackageInfoFlag
        )

        // Match both PackageInfo for there signatures
        return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
            sysPackageInfo.signatures[0] == targetPkgInfo.signatures[0]
        } else {
            targetPkgInfo.signingInfo == sysPackageInfo.signingInfo
        }
    }

    /**
     * Check if the application has the root permission to execute command in the shell.
     *
     * @return Boolean
     **/
    private fun checkShellRootPermission(): Boolean {
        var os: DataOutputStream? = null
        var process: Process? = null
        try {
            process = Runtime.getRuntime().exec("su")
            os = DataOutputStream(process.outputStream)

            os.write("echo root".toByteArray())
            os.writeBytes("\n")
            os.flush()

            os.writeBytes("exit\n")
            os.flush()

            val result: Int = process.waitFor()

            return result == 0
        } catch (e: IOException) {
            e.printStackTrace()
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            try {
                os?.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }

            process?.destroy()
        }

        return false
    }

    /**
     * Install the APK at the inputted file path.
     * If install silently, then the app need to fulfil one of the condition
     * 1. The app is a system application.
     * 2. The app has root permission during shell execution.
     *
     **/
    fun installApk(call: MethodCall, result: Result) {
        val filePath: String? = call.argument<String?>("filePath")
        val silently: Boolean? = call.argument<Boolean?>("silently")

        if (filePath == null) {
            result.error(
                "MISSING_ARGUMENT",
                "Missing filePath argument.",
                "Please call this method with filePath argument."
            )
            return
        }
        if (silently == true) {
            if (isSystemApplication(applicationContext.packageManager, applicationContext.packageName)
                || checkShellRootPermission()
            ) {
                result.success(installApkSilently(filePath))
            }
        } else {
            result.success(installNormal(filePath))
        }
    }

    /**
     * Install the apk with Android Intent.
     **/
    private fun installNormal(filePath: String): Boolean {
        val file = File(filePath)
        if (!file.exists()) {
            return false
        }
        val intent = Intent(Intent.ACTION_VIEW)
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            val contentUri: Uri = FileProvider.getUriForFile(
                applicationContext,
                applicationContext.packageName + ".fileProvider", file
            )
            intent.setDataAndType(contentUri, "application/vnd.android.package-archive")
        } else {
            intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive")
        }

        mActivity?.startActivity(intent)
        return true
    }

    /**
     * Install the apk silently with pm command execution.
     *
     * @param filePath String
     * @return Boolean
     **/
    private fun installApkSilently(filePath: String): Boolean {
        return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            installAppSilentBelow24(filePath)
        } else {
            installAppSilentAbove24(applicationContext.packageName, filePath)
        }
    }

    /**
     * Install the apk silently with pm command execution below API level 24.
     *
     * @param filePath String
     * @return Boolean
     **/
    private fun installAppSilentBelow24(filePath: String): Boolean {
        val file = File(filePath)
        if (!file.exists()) {
            return false
        }

        val command = "pm install -r ${file.path.replace(" ", "\\ ")}"
        val commandResult: CommandResult = execCommand(
            command, !isSystemApplication(applicationContext.packageManager, applicationContext.packageName), true
        )

        return (commandResult.successMsg != null
                && commandResult.successMsg!!.lowercase(Locale.ROOT).contains("success"))
    }

    /**
     * Install the apk silently with pm command execution above API level 24 (include).
     *
     * @param packageName String
     * @param filePath String
     * @return Boolean
     **/
    private fun installAppSilentAbove24(packageName: String, filePath: String): Boolean {
        val file = File(filePath)
        if (!file.exists()) {
            return false
        }

        val command = "pm install -i $packageName $filePath"
        val commandResult: CommandResult = execCommand(command, isRoot = true, isNeedResultMsg = false)
        return (commandResult.successMsg != null
                && commandResult.successMsg!!.lowercase(Locale.ROOT).contains("success"))
    }

    /**
     * Check if the app has root permission during shell execution
     *
     * @return Boolean
     **/
    fun checkRootPermission(): Boolean {
        val commandResult = execCommand("echo root", isRoot = true, isNeedResultMsg = true)
        return commandResult.result == 0
    }

    /**
     * Execute the command
     *
     * @param command
     * @param isRoot Execute the command with root permission or not.
     * @param isNeedResultMsg Do the result need to be return or not.
     * @return CommandResult
     **/
    private fun execCommand(command: String, isRoot: Boolean, isNeedResultMsg: Boolean): CommandResult {
        var result: Int? = null
        if (command.isEmpty()) {
            return CommandResult(null, null, null)
        }

        var process: Process? = null
        var successMsg: String? = null
        var errorMsg: String? = null

        try {
            process = if (isRoot)
                Runtime.getRuntime().exec(arrayOf("su", "-c", command))
            else
                Runtime.getRuntime().exec(command)

            result = process.waitFor()
            // get command result
            if (isNeedResultMsg) {
                successMsg = process.inputStream.bufferedReader().use(BufferedReader::readText)
                errorMsg = process.errorStream.bufferedReader().use(BufferedReader::readText)
            }
        } catch (e: IOException) {
            e.printStackTrace()
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            process?.destroy()
        }

        return CommandResult(result, successMsg, errorMsg)
    }

    class CommandResult(var result: Int?, var successMsg: String?, var errorMsg: String?)
}